Skip to content

feat(multisig-client): offline switch-guardian proposal creation (#433) - #436

Merged
haseebrabbani merged 2 commits into
mainfrom
433-offline-switch-proposal
Sep 1, 2026
Merged

feat(multisig-client): offline switch-guardian proposal creation (#433)#436
haseebrabbani merged 2 commits into
mainfrom
433-offline-switch-proposal

Conversation

@haseebrabbani

Copy link
Copy Markdown
Collaborator

Closes #433.

Adds createSwitchGuardianProposalOffline(newGuardianEndpoint, newGuardianPubkey, { nonce }?) to the TS client — the creation counterpart to the existing importProposal / signProposalOffline / executeProposal trio, mirroring the Rust create_proposal_offline. Nothing is pushed to the current GUARDIAN, so an account can leave an unreachable operator (the 0xMiden/wallet#782 scenario): the method syncs with the Miden node (Rust sync_network_only parity), verifies the new endpoint's /pubkey commitment, builds and signs the summary locally, caches the proposal, and returns the ExportedProposal (proposer signature included) for side-channel cosigning.

Design notes:

  • The verify/build/metadata step is extracted into a private buildSwitchGuardianSummary shared with createSwitchGuardianProposal, so the online and offline proposals for the same operation cannot drift.
  • Caching and proposer signing reuse importProposal + signProposalOffline end to end rather than re-implementing them, so the local copy carries exactly the guarantees a cosigner-imported one does.
  • computeCommitmentFromTxSummary is now exported (second ask in the issue) and returns normalized hex, directly comparable to ExportedProposal.commitment / Proposal.id.
  • Only switch-guardian proposals can be created offline — every other type needs a GUARDIAN ack at execution (same restriction as Rust, moved to compile time by the per-type method).

Add createSwitchGuardianProposalOffline: build, sign, and cache a
switch-guardian proposal without contacting the current GUARDIAN, so an
account can leave an unreachable operator (mirrors the Rust
create_proposal_offline, including its pre-build network sync). The
returned ExportedProposal carries the proposer's signature and feeds the
existing importProposal / signProposalOffline / executeProposal trio.

The build step is shared with createSwitchGuardianProposal via
buildSwitchGuardianSummary so the online and offline proposals for the
same operation cannot drift, and the offline method reuses
importProposal + signProposalOffline for caching and signing instead of
re-implementing them.

Also export computeCommitmentFromTxSummary for hand-rolled
export/import flows, now returning normalized hex directly comparable
to proposal ids.
@haseebrabbani
haseebrabbani requested a review from zeljkoX as a code owner August 27, 2026 23:32
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: d0451750-c677-4cfc-8641-caa42879a105


Comment @coderabbitai help to get the list of available commands.

Comment thread packages/miden-multisig-client/src/multisig.test.ts Fixed

@zeljkoX zeljkoX left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Added one minor commment.

Comment on lines +1012 to +1013
const webClient = await this.getRawClient();
await retryRpcRead(() => webClient.syncState(), this.rpcConfig);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rust’s sync_network_only() also refreshes the cached account after syncing. Here, webClient.syncState() updates the WASM store but leaves this.account, this.threshold, this.signerCommitments, and this.procedureThresholds at their previous values.

That means the summary can be built against the latest account state while readiness and signature validation use stale configuration. For example, after the on-chain threshold changes from 1 to 2, this client could incorrectly consider the proposal ready with one signature and fail only during submission.

Please reload the account from the store and call refreshConfigFromAccount() after this sync, ideally through a private syncNetworkOnly() helper shared with the post-switch sync in executeProposal.

…sync (#436 review)

The offline create synced the WASM store but left this.account and the
cached threshold/signer/procedure config stale, so readiness and
requiredSignatures could be computed from outdated state (e.g. a
threshold raised on-chain to 2 would still mark the proposal ready with
one signature, failing only at submission). Extract a private
syncNetworkOnly() that syncs and then reloads the account via
refreshConfigFromAccount — full parity with the Rust sync_network_only
— and share it with executeProposal's post-switch sync.

Also address the CodeQL note on the test fetch stub by matching the
parsed URL origin instead of a substring prefix.
@haseebrabbani
haseebrabbani merged commit fc8bf65 into main Sep 1, 2026
26 checks passed
@haseebrabbani
haseebrabbani deleted the 433-offline-switch-proposal branch September 1, 2026 20:55
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

3 participants